import { Icon, useCall, useEffectState, useForkRef, useRaf, useSimpleMemo, useToggle } from '@/components';
import { forkHandler, nextTick, uid } from '@/utils';
import { Button } from 'antd';
import clsx from 'clsx';
import * as PropTypes from 'prop-types';
import React, {
  Children,
  cloneElement,
  createContext,
  forwardRef,
  isValidElement,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { findDOMNode } from 'react-dom';
import styles from './index.less';

const Context = createContext({ expanded: false, toggle: null, instanceId: null });

const contextMap = new Map();

/** 可展开收缩组件 */
function Expandable(props) {
  const { expanded, onToggle, defaultExpanded = false, children } = props;
  const [expandedControl, setExpandControl] = useEffectState(expanded);
  useEffect(() => {
    if (defaultExpanded) setExpandControl(true);
  }, []);
  const [open, toggle] = useToggle(expandedControl, onToggle);
  const instanceId = useMemo(() => `Expandable_${uid()}`, []);

  const ctx = useSimpleMemo({ expanded: open, toggle: toggle, instanceId });
  useEffect(() => {
    contextMap.set(instanceId, ctx);
    return () => {
      contextMap.delete(instanceId);
    };
  }, []);

  return <Context.Provider value={ctx}>{typeof children === 'function' ? children(ctx) : children}</Context.Provider>;
}

Expandable.propTypes = {
  /** 展开状态 */
  expanded: PropTypes.bool,

  /** 展开状态改变事件 */
  onToggle: PropTypes.any,

  /** 默认展开 */
  defaultExpanded: PropTypes.bool,

  children: PropTypes.any,
};

/** */
function ExpandTrigger({ children, expandedClassName, ...props }) {
  const ctx = useContext(Context);

  if (isValidElement(children) && Children.count(children) === 1) {
    return cloneElement(children, { onClick: forkHandler(children.props.onClick, ctx.toggle, true) });
  }

  if (typeof children === 'function') return children(ctx);

  if (!children) {
    return (
      <Button
        type='link'
        size='small'
        {...props}
        className={clsx(styles.expandBtn, { [styles.expanded]: ctx.expanded }, props.className, {
          [expandedClassName]: ctx.expanded,
        })}
        onClick={useCall(forkHandler(props.onClick, ctx.toggle, true))}
      >
        {ctx.expanded ? '收起' : '展开'}
        <Icon type='down' className={styles.expandIcon} />
      </Button>
    );
  }

  return children;
}

function ExpandContent(props, propRef) {
  const { instanceId } = useContext(Context);
  const { component: Comp = 'div', children, ...rest } = props;
  const ref = useRef();
  const elRef = useForkRef(ref, propRef);

  const ctx = useContext(Context);
  const [transitioning, setTransitioning] = useState(false);
  const [contentHeight, setHeight] = useState(-1);

  const { expanded } = ctx;
  const calc = expanded && contentHeight < 0;
  const height = expanded ? Math.max(0, contentHeight) : 0;

  const rRef = useRef();
  const eRef = useRef();

  const getHeight = () => {
    if (!ref.current) return;
    if (rRef.current !== ref.current) {
      rRef.current = ref.current;
      eRef.current = findDOMNode(ref.current);
    }

    /** @type HTMLElement */
    const el = eRef.current;
    const newHeight = el.getBoundingClientRect?.().height ?? el.offsetHeight;
    setHeight((prevHeight) => {
      if (prevHeight === newHeight) setTransitioning(false);
      return newHeight;
    });
  };

  useEffect(() => {
    if (!transitioning) setTransitioning(true);
  }, [expanded]);

  useRaf(() => {
    if (expanded && !transitioning) getHeight();
  });

  useEffect(() => {
    if (calc) {
      setHeight(0);
      nextTick(() => {
        getHeight();
      });
    }
  }, [calc]);

  const handleTransEnd = () => {
    setTransitioning(false);
  };

  return (
    <div
      data-expand={instanceId}
      className={clsx(styles.expandableContent, { [styles.calc]: calc })}
      style={{ height }}
      onTransitionEnd={handleTransEnd}
    >
      {typeof children === 'function' ? children(ctx) : <Comp ref={elRef} {...rest} children={children} />}
    </div>
  );
}

ExpandContent = forwardRef(ExpandContent);

Expandable.Trigger = ExpandTrigger;
Expandable.Content = ExpandContent;

export function useExpandable() {
  return useContext(Context);
}

const getContextForElement = (elementOrInstanceId) => {
  let element = elementOrInstanceId;
  if (!element) return null;
  let instanceId = typeof element === 'string' ? element : element.closest('[data-expand]')?.dataset.expand;
  return (instanceId && contextMap.get(instanceId)) ?? null;
};

const toggleAndWaitAnimation = (context) => {
  const instanceElement = document.querySelector(`[data-expand="${context.instanceId}"]`);
  if (!instanceElement) {
    context.toggle();
    return true;
  }

  return new Promise((resolve) => {
    let resolved = false;
    const onceEnd = () => {
      if (resolved) return;
      instanceElement.removeEventListener('transitionend', onceEnd);
      resolve(true);
    };
    instanceElement.addEventListener('transitionend', onceEnd);
    setTimeout(onceEnd, 1e3);
    context.toggle();
  });
};

export function toggleFor(elementOrInstanceId) {
  const context = getContextForElement(elementOrInstanceId);
  if (!context) return false;
  return toggleAndWaitAnimation(context);
}

export function expandFor(elementOrInstanceId) {
  const context = getContextForElement(elementOrInstanceId);
  if (!context || !context.expanded) return false;
  return toggleAndWaitAnimation(context);
}

export function collapseFor(elementOrInstanceId) {
  const context = getContextForElement(elementOrInstanceId);
  if (!context || context.expanded) return false;
  return toggleAndWaitAnimation(context);
}

export default Expandable;
